﻿using System;
using System.Collections.Generic;
using System.Threading;

public abstract class ExpiringCache<TKey, TValue> : IDisposable
{
    public readonly TimeSpan LifeSpan;
    
    protected readonly object locker = new object();

    protected readonly Dictionary<TKey, TValue> cache =
        new Dictionary<TKey, TValue>();

    private readonly Queue<KeyValuePair<TKey, long>> queue =
        new Queue<KeyValuePair<TKey, long>>();

    private volatile bool dispose;

    private readonly Thread cleaningThread;

    private readonly long lifeSpanTicks;

    public ExpiringCache(TimeSpan lifeSpan)
    {
        if (lifeSpan.Ticks == 0)
        {
            throw new ArgumentOutOfRangeException
                ("lifeSpan", "Must be greater than zero.");
        }

        this.LifeSpan = lifeSpan;
        this.lifeSpanTicks = lifeSpan.Ticks;

        this.cleaningThread = new Thread(() =>
        {
            while (!this.dispose)
            {
                this.RemoveExpired();
                Thread.Sleep(1);
            }
        })
        {
            Name = string.Format("{0}[{1}].{2}()/",
                        this.GetType(),
                        this.GetHashCode(),
                        System.Reflection.MethodInfo.GetCurrentMethod().Name),
            Priority = ThreadPriority.Normal,

            // Do not prevent a process from terminating

            IsBackground = true
        };

        this.cleaningThread.Start();
    }

    private void RemoveExpired()
    {
        lock (this.locker)
        {
            if (this.queue.Count == 0)
            {
                return;
            }

            var pair = this.queue.Peek();

            if (pair.Value <= DateTime.Now.Ticks)
            {
                this.cache.Remove(pair.Key);
                this.queue.Dequeue();
            }
        }
    }

    public void Add(TKey key, TValue value)
    {
        lock (this.locker)
        {
            this.cache.Add(key, value);

            this.queue.Enqueue(new KeyValuePair<TKey, long>
                (key, DateTime.Now.Ticks + this.lifeSpanTicks));
        }
    }

    public void Dispose()
    {
        this.dispose = true;
    }
}

public class ExpiringObjectCache<TKey, TValue>
    : ExpiringCache<TKey, TValue> where TValue : class
{
    public ExpiringObjectCache(TimeSpan lifespan)
        : base(lifespan)
    { }

    public TValue this[TKey key]
    {
        get
        {
            lock (this.locker)
            {
                if (this.cache.ContainsKey(key))
                {
                    return this.cache[key];
                }
            }

            return null;
        }
    }
}

public class ExpiringValueCache<TKey, TValue>
    : ExpiringCache<TKey, TValue> where TValue : struct
{
    public ExpiringValueCache(TimeSpan lifespan)
        : base(lifespan)
    { }

    public TValue? this[TKey key]
    {
        get
        {
            lock (this.locker)
            {
                if (this.cache.ContainsKey(key))
                {
                    return this.cache[key];
                }
            }

            return null;
        }
    }
}
